var http = require('http');
var net = require('net');
var url = require('url');
let https = require('https');
let fs = require('fs');
let os = require('os');
let ws = require('ws');
let zlib = require('zlib');
let {exec: asyncExec} = require('child_process');
let G = {};

function getLocalIp() {
    let interfaces = os.networkInterfaces();
    for(let name in interfaces) {
        for(let info of interfaces[name]) {
            if(info.family == 'IPv4' && info.address != '127.0.0.1' && !info.internal) {
                return info.address;
            }
        }
    }
}

function exec(command, options = {}, log = false) {
    if(Array.isArray(command)) {
        command = command.join(' ');
    }
    log && fs.appendFileSync('command.txt',command+os.EOL);
    return new Promise((rs, rj)=>{
        asyncExec(command, options, (err, sto, ste)=>{
            if(err) {
                rj(err);
            } else {
                rs(ste || sto);
            }
        });
    });
}

function fExists(path) {
    try {
        fs.accessSync(path, fs.constants.F_OK);
        return true;
    } catch(e) {
        return false;
    }
}

function sleep(intval) {
    return new Promise((rs,rj)=>{
        setTimeout(()=>{rs()},intval);
    });
}

function isIPHost(host) {
    return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)*$/.test(host);
}

function getBaseHost(host) {
    let baseHost;
    if(isIPHost(host)) {
        baseHost = host.split(':')[0];
    } else {
        let arr = host.split(':')[0].split('.');
        if(arr.length > 2) {
            arr.shift()
        }
        baseHost = arr.join('.');
    }
    return baseHost;
}

function getHostConfigFile(baseHost) {
    let cfg = ['',
    '[alt_ext]',
    `subjectAltName = @alt_names`,
    '',
    '[alt_names]',
    ];
    if(isIPHost(baseHost)) {
        cfg.push(`IP.1 = ${baseHost}`);
    } else {
        cfg.push(`DNS.1 = ${baseHost}`,`DNS.2 = *.${baseHost}`);
    }
    let extCfg = Buffer.from(cfg.join(os.EOL));
    let cfgPath = `${G.certDir}/${baseHost}.cfg`;
    fs.writeFileSync(cfgPath, Buffer.concat([G.certConfig,extCfg]));
    return cfgPath;
}

function getCAConfigFile() {
    let cfgPath = `${G.certDir}/ca.cfg`;
    if(!fExists(cfgPath)) {
        let cfg = ['',
        '[ext]',
        `basicConstraints=CA:TRUE`,
        ];
        let extCfg = Buffer.from(cfg.join(os.EOL));
        fs.writeFileSync(cfgPath, Buffer.concat([G.certConfig,extCfg]));
    }
    return cfgPath;
}

async function generateCA() {
    let dir = G.certDir;
    if(fExists(`${dir}/ca.crt`) && fExists(`${dir}/ca.key`)) {
        return;
    }
    let config = getCAConfigFile();
    await exec(`openssl genrsa -out ca.key 2048`,{cwd:dir});
    await exec(['openssl','req',`-config ${config} -extensions ext`,'-subj',
        `"/C=CN/ST=SH/L=SH/O=Gin/OU=Gin/CN=Gin CA/emailAddress=gin@gin.com"`,
        '-new','-x509','-days','3650','-key','ca.key','-out','ca.crt'],{cwd:dir});
}

async function init() {
    G.servers = {};
    G.wsServers = {};
    G.delimiter = '|`|&|$|#|';
    G.transId = 0;
    G.localIp = getLocalIp();
    G.certDir = `${__dirname}/certificateCache`;
    if(!fExists(G.certDir)) {
        fs.mkdirSync(G.certDir);
    }
    let opensslDirInfo = await exec('openssl version -d');
    let opensslDir = opensslDirInfo.match(/"([a-z\/ ]+)"/)[1];
    G.certConfig = fs.readFileSync(`${opensslDir}/openssl.cnf`);
    await generateCA();
    if(!fExists(`${G.certDir}/server.key`)) {
        await exec(`openssl genrsa -out server.key 2048`,{cwd:G.certDir});
    }
    G.certGenerator = (() => {
        let lockList = {};
        return async function generate(baseHost) {
            while(true) {
                if(baseHost in lockList) {
                    await sleep(0);
                } else {
                    break;
                }
            }
            lockList[baseHost] = true;
            let dir = G.certDir;
        
            let hostCsr = `${baseHost}.csr`;
            let hostCrt = `${baseHost}.crt`;
            let keyPath = `${dir}/server.key`;
            let crtPath = `${dir}/${hostCrt}`;
            if(fExists(crtPath)) {
                delete lockList[baseHost];
                return {key: keyPath, crt: crtPath};
            }
        
            let cfgPath = getHostConfigFile(baseHost);
            await exec(['openssl','req','-subj',
                `"/C=CN/ST=SH/L=SH/O=Gin/OU=Gin/CN=${isIPHost(baseHost)?'':'*.'}${baseHost}/emailAddress=test@${baseHost}"`,
                '-reqexts alt_ext',`-config ${cfgPath}`,'-sha256',
                '-new','-key',keyPath,'-out',hostCsr],{cwd:dir});
            await exec(['openssl','x509','-req','-days','3650','-in',hostCsr,
                '-extensions alt_ext',`-extfile ${cfgPath}`,'-sha256',
                '-CA','ca.crt','-CAkey','ca.key','-set_serial','01','-out',hostCrt],{cwd:dir});
            delete lockList[baseHost];
            return {key: keyPath, crt: crtPath};
        }
    })();
}

function request(cReq, cRes) {
    var u = url.parse(cReq.url);
    let host = u.hostname, port = u.port || 80;
    if(u.hostname == 'https-proxy.gin') {
        host = 'localhost';
        port = 5229;
    }
    var options = {
        hostname : host, 
        port     : port,
        path     : u.path,       
        method     : cReq.method,
        headers     : cReq.headers
    };

    var pReq = http.request(options, function(pRes) {
        cRes.writeHead(pRes.statusCode, pRes.headers);
        pRes.pipe(cRes);
    }).on('error', function(e) {
        cRes.end();
    });

    cReq.pipe(pReq);
}

async function connect(originalReq, originalSock) {
    let server = await createHttpsServer(originalReq.headers.host, reqHandler, wsHandler);
    
    let params = [];
    if(server) {
        params.push(server.port);
    } else {
        params.push(443, originalReq.headers.host)
    }
    let proxySock = net.connect(...params, function() {
        originalSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
        proxySock.pipe(originalSock);
    }).on('error', function(e) {
        originalSock.end();
    });

    originalSock.pipe(proxySock);
}

function wsHandler(server){
    let queue = [];
    let wss = new ws.Server({server});
    wss.on('connection', (socket, req)=>{
        let protocol = (req.connection && req.connection.encrypted) ? 'wss' : 'ws';
        let proxyWs = new ws(`${protocol}://${req.headers.host}${req.url}`,{
            headers : filterHeader(req.headers),
        });
        proxyWs.on('open',()=>{
            send();
        });
        proxyWs.on('message',(data)=>{
            socket.send(data);
        });
        socket.on('message',(data)=>{
            let transId = G.transId++;
            mWsSend('req',transId,`wss://${req.headers.host}${req.url}`,data.toString());
            send(data);
        });
        function send(data) {
            data && queue.push(data);
            if(proxyWs.readyState === 1) {
                while(queue.length) {
                    proxyWs.send(queue.shift());
                }
            }
        }
        function filterHeader(headers) {
            let originHeaders = Object.assign({}, headers);
            Object.keys(originHeaders).forEach((key) => {
                if (/sec-websocket/ig.test(key)) {
                    delete originHeaders[key];
                }
            });
            delete originHeaders.connection;
            delete originHeaders.upgrade;
            return originHeaders;
        }
    });
    wss.on('headers', (headers, req)=>{
    });
    wss.on('close', (data)=>{
    });
    wss.on('error',(error)=>{
    });
    return wss;
}

function reqHandler(proxyReq, proxyRes){
    let transId = G.transId++;
    let [host, port = 443] = proxyReq.headers.host.split(':');
    let headers = Object.assign({},proxyReq.headers);
    for(let key in headers) {
        if(key.toLowerCase() == 'accept-encoding') {
            headers[key] = 'gzip, deflate';
            break;
        }
    }
    let options = {
        hostname : host, 
        port     : port,
        path     : proxyReq.url,       
        method     : proxyReq.method,
        headers     : headers,
        rejectUnauthorized : false,
    };
    let proxyData = [];
    proxyReq.on('data',function(d){
        proxyData.push(d);
    });
    proxyReq.on('end',async function(){
        let realReq = https.request(options, function(realRes){
            let realData = [];
            realRes.on('data',function(d){
                realData.push(d);
            });
            realRes.on('end',function(){
                proxyRes.writeHead(realRes.statusCode, realRes.headers);
                realData = Buffer.concat(realData);
                let showData = realData;
                switch(realRes.headers['content-encoding']) {
                    case 'gzip':
                        showData = zlib.gunzipSync(realData);
                        break;
                    case 'deflate':
                        showData = zlib.inflateSync(realData);
                        break;
                    default:
                }
                mWsSend('res',transId,``,showData.toString());
                proxyRes.end(realData);
            });
        });
        realReq.on('error',function(e){
        });
        proxyData = Buffer.concat(proxyData);
        mWsSend('req',transId,`https://${proxyReq.headers.host}${proxyReq.url}`,proxyData.toString());
        realReq.end(proxyData);
    });
}

async function createHttpsServer(host, reqHandler, wsHandler) {
    let baseHost = getBaseHost(host);
    if(!(baseHost in G.servers)) {
        let cert = await G.certGenerator(baseHost);
        G.servers[baseHost] = await new Promise((rs, rj)=>{
            const options = {
              key: fs.readFileSync(cert.key),
              cert: fs.readFileSync(cert.crt)
            };
            let server = https.createServer(options, reqHandler).listen(0, function() {
                rs({port:server.address().port, server});
            });
            server.on('upgrade',()=>{
                if(!(baseHost in G.wsServers)) {
                    G.wsServers[baseHost] = wsHandler(server);
                }
            });
        });
    }
    return G.servers[baseHost];
}

function releaseServer(baseHost) {
    if(baseHost in G.servers) {
        G.servers[baseHost].server.close(()=>{
            delete G.servers[baseHost];
        });
    }
}

let monitorSockets = [];
function mWsSend() {
    let data = [...arguments].join(G.delimiter);
    monitorSockets.forEach((socket)=>{
        socket.send(data);
    });
}

(async ()=>{
    await init();
    let server = http.createServer();
    server.on('request', request);
    server.on('connect', connect);
    server.listen(5227, G.localIp, ()=>{
        console.log('proxy server is running on prot 5227');

        let monitorSever = http.createServer(function(req, res){
            if(req.url === '/favicon.ico') {
                res.end('');
            }
            if(req.url === '/crt') {
                res.setHeader('Content-disposition', 'attachment; filename=ca.crt');
                fs.createReadStream(`${G.certDir}/ca.crt`).pipe(res);
            }
            if(req.url === '/') {
                res.setHeader('Content-type', 'text/html; charset=utf8');
                res.end(fs.readFileSync('./proxy.html').toString()
                    .replace(/__localIp__/gm,G.localIp));
            }
        }).listen(5229,()=>{
            console.log('monitor url is http://https-proxy.gin');
        });
        let monitorWs = new ws.Server({server:monitorSever});
        monitorWs.on('connection', function(socket){
            socket.send(`delimiter:${G.delimiter}`);
            monitorSockets.push(socket);
        });
    });
})();

async function testCert(host) {
    let cert = await G.certGenerator(host);
    const options = {
      key: fs.readFileSync(cert.key),
      cert: fs.readFileSync(cert.crt)
    };
    
    https.createServer(options, (req, res) => {
      res.writeHead(200);
      res.end(`this is not real ${host}`);
    }).listen(5230,()=>{
        console.log(`before visit 'https://${host}:5230',
        don't forget add '${host} 127.0.0.1' to the hosts file`);
    });
}